import crypto from 'crypto';
import fs, { CopyOptions } from 'fs-extra';
import path from 'path';
import zlib from 'zlib';
import { diskinfo } from '@dropb/diskinfo';
import archiver from 'archiver';
import moment from 'moment';
import xml2js from "xml2js";
import { Cmd } from './Cmd.js';
import { sendBuildFailureAlert } from './alert.js';
import { pipeline } from 'stream';
import { promisify } from 'util';
import { Hudson } from './hudson.js';
import { toolchain } from '../toolchain.js';
import { execSync } from 'child_process';
import wait from 'wait';

export declare type FindFileMode = '+' | '-';

export async function checkDiskAvailable(p: string, minByte: number) {
    const pp = path.parse(p);
    let info = await diskinfo(pp.root);
    if(info.avail < minByte) {
        console.log(info);
        await sendBuildFailureAlert('磁盘剩余空间不足，请及时清理！');
        process.exit(1);
    }
}

export function gb2byte(gb: number): number {
    return gb * 1024 * 1024 * 1024;
}

export async function fileSize(file: string): Promise<number> {
    const fstat = await fs.stat(file);
    return fstat.size;
}

export function eitherPath(a: string, b: string): string {
    if (fs.existsSync(a)) return a;
    return b;
}

export function getHudsonLogURL(): string {
    return `http://${toolchain.params.ip}:8282/hudson/job/${Hudson.getJobName()}/${Hudson.getBuildNumber()}/consoleText`;
}

export async function safeCopy(src: string, dest: string, options?: CopyOptions): Promise<void> {
    if (fs.existsSync(src)) {
        await fs.copy(src, dest, options);
    }
}

export async function safeCopyFile(src: string, dest: string): Promise<void> {
    if (fs.existsSync(src)) {
        const pd = path.dirname(dest);
        await fs.ensureDir(pd);
        await fs.copyFile(src, dest);
    } else {
        console.log('skip copy cause src not exists:', src);
    }
}

export async function findFiles(root: string, exts: string[], mode: FindFileMode): Promise<string[]> {
    let out: string[] = [];
    if(exts) {
        for(let i = 0, len = exts.length; i < len; i++) {
            exts[i] = exts[i].toLowerCase();
        }
    }
    await findFilesInternal(root, exts, mode, out);
    return out;
}

async function findFilesInternal(root: string, exts: string[], mode: FindFileMode, out: string[]): Promise<void> {
    if (!fs.existsSync(root)) return;
    const files = await fs.readdir(root);
    for(let f of files) {
        let file = path.join(root, f);
        const fstat = await fs.stat(file);
        if(fstat.isDirectory()) {
            await findFilesInternal(file, exts, mode, out);
        } else if(!exts || (mode == '+') == exts.includes(path.extname(f).toLowerCase())) {
            out.push(file);
        }
    }
}

export function joinURLs(...urls: string[]): string {
    return urls.map(v => v.replace(/^\/+/, '').replace(/\/+$/, '')).join('/');
}

export async function parseXml(s: string) {
    const promise = await new Promise((resolve, reject) => {
        const parser = new xml2js.Parser(); //{ explicitArray: false }
        parser.parseString(s, (error, result) => {
            if (error) reject(error);
            else resolve(result);
        });
    });
    return promise;
}

export async function download(url: string, output: string): Promise<number> {    
    const cmd = new Cmd();
    let exitcode = 0;
    const out = await cmd.run('curl', ['-o', output, '-L', '--insecure', url], { silent: true }).catch((e) => {
        console.error(e);
        exitcode = 1;
    });
    if (out != null) {
        exitcode = out;
    }
    return exitcode;
}

/**附加md5并返回新的文件名 */
export async function appendMd5(file: string, md5Len: number, ext?: string, connectChar = '_'): Promise<{ fileMd5: string, newFileName: string }> {
    const name = path.basename(file);
    if (!ext) ext = path.extname(name);
    const fileMd5 = md5(await fs.readFile(file));
    const newFileName = name.replace(ext, connectChar + fileMd5.substring(0, md5Len) + ext);
    const newFile = file.replace(name, newFileName);
    await fs.rename(file, newFile);
    return { fileMd5, newFileName };
}

export function replaceOrThrow(s: string, searchValue: string | RegExp, replaceValue: string): string {
    if (s.search(searchValue) < 0) {
        console.error('------Input string which fails replacement--------');
        console.error(s);
        console.error('--------------------------------------------------');
        throw new Error('Replace failed for:' + searchValue);
    }
    return s.replace(searchValue, replaceValue);
}

export function md5(str: crypto.BinaryLike): string {
    const md5 = crypto.createHash('md5');
    md5.update(str);
    str = md5.digest('hex');
    return str;  
}

export async function replaceInFile(file: string, searchValue: string | RegExp, replaceVale: string): Promise<boolean> {
    const content = await fs.readFile(file, 'utf-8');
    const idx = typeof searchValue == 'string' ? content.indexOf(searchValue) : content.search(searchValue);
    if (idx >= 0) {
        const newContent = content.replace(searchValue, replaceVale);
        if (newContent != content) {
            await fs.writeFile(file, newContent, 'utf-8');
        }
        return true;
    }
    return false;
}

export async function replaceInFileOrThrow(file: string, searchValue: string | RegExp, replaceVale: string): Promise<void> {
    const replaced = await replaceInFile(file, searchValue, replaceVale);
    if (!replaced) {
        throw `replace in file failed: ${file}, searchValue: ${searchValue}, replaceValue: ${replaceVale}`;
    }
}

export function writeUTF8withBom(file: string, content: string) {
    fs.writeFileSync(file, '\ufeff' + content, 'utf-8');
}

export async function readNumber(file: string): Promise<number> {
    const content = await fs.readFile(file, 'utf-8');
    return Number(content);
}

/**
 * 制作压缩包
 * @param targetName 生成的压缩包名字/路径
 * @param format 压缩格式
 * @param source 压缩包内容源目录，默认为当前目录，源目录底下所有文件将被打包到压缩包中的同名文件夹下
 */
export async function makeArchive(targetName: string, format: archiver.Format, source?: string): Promise<void> {
    return new Promise(async (resolve, reject)=>{
        await fs.ensureDir(path.dirname(targetName));
        const archive = archiver(format, {
            zlib: { level: 9 }
        });
        if(!source) source = process.cwd();
        const output = fs.createWriteStream(targetName);
        archive.pipe(output);
        const srcStat = await fs.stat(source);
        if (srcStat.isDirectory()) {
            archive.directory(source, path.basename(source));
        } else {
            archive.file(source, { name: path.basename(source) });
        }
        output.once('finish', ()=>{
            resolve();
        });
        output.once('error', (err)=>{
            reject(err);
        });
        await archive.finalize();
    });
}

export async function unzipTo(dirname: string, zipfilename: string): Promise<void> {
    await fs.ensureDir(dirname);
    await new Cmd().run('tar', ['-xf', zipfilename, '-C', dirname]);
}

export async function zipTo(outaarpath: string, tmppath: string): Promise<void> {
    await fs.unlink(outaarpath);
    await new Cmd().run('zip', ['-r', '-j', outaarpath, tmppath]);
}

export async function gzip(inputFile: string, outputFile: string): Promise<void> {
    const gz = zlib.createGzip();
    const rs = fs.createReadStream(inputFile);
    const ws = fs.createWriteStream(outputFile);
    const pplp = promisify(pipeline);
    await pplp(rs, gz, ws);
}

export function getModifyTime(file: string): string {
    let fsStat = fs.statSync(file);
    return moment(fsStat.mtime).format('YYYY_MM_DD_HH_mm_ss');
}

export function findOption(options: string[], optionName: string): string | null {
    for (let i = 0, len = options.length; i < len; i++) {
        if (options[i] == optionName) {
            return options[i + 1];
        }
    }
    return null;
}

/**设置一组选项，并返回新选项数组。不影响原选项数组。 */
export function setOption(options: string[], optionName: string, optionValue:string): string[] {
    const out = options.slice();
    for (let i = 0, len = out.length; i < len; i++) {
        if (out[i] == optionName) {
            out[i + 1] = optionValue;
            break;
        }
    }
    return out;
}

/**
 * 读取version.txt并解析内容。大部分项目的version.txt仅包含资源版本号，新版本的小游戏项目（比如斗破小游戏）还包含了json版本号。
 * @param txtFile version.txt文件路径
 * @returns 一个形如[资源版本号, json版本号]的数组。
 */
export async function readVersionTxt(txtFile: string): Promise<[verCode: string, jsonMd5?: string]> {
    const content = await fs.readFile(txtFile, 'utf-8');
    return content.split(/\r?\n/) as [verCode: string, jsonMd5?: string];
}

export async function ensureRemoveDir(dir: string): Promise<void> {
    await fs.remove(dir).catch(async err =>  {
        console.error(err);
        const mch = err.message.match(/EBUSY: resource busy or locked, unlink '(.+)'/);
        if (mch) {
            const stucked = mch[1];
            if (fs.existsSync(stucked)) {
                console.log('try to delete stucked file: ' + stucked);
                // windows下使用handle.exe来删除，从下面网址下载handle并添加到环境变量中
                // https://learn.microsoft.com/en-us/sysinternals/downloads/handle
                const result = execSync(`handle.exe ${stucked}`).toString();
                console.log(result);
                const pidMch = result.match(/pid:\s+(\d+)/);
                if (pidMch) {
                    const pid = pidMch[1];
                    console.log('try to kill pid: ' + pid);
                    const killResult = execSync(`taskkill /F /PID ${pid}`).toString();
                    console.log(killResult);

                    console.log('wait and remive again');
                    await wait(1000);
                    await fs.remove(dir);
                }
            }
        }
    });
}